Skip to content

stack exec: default to local emulator, add --cloud opt-out#1422

Open
BilalG1 wants to merge 5 commits intodevfrom
cli-exec-changes
Open

stack exec: default to local emulator, add --cloud opt-out#1422
BilalG1 wants to merge 5 commits intodevfrom
cli-exec-changes

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented May 8, 2026

Summary

  • stack exec now defaults to the local emulator: signs in as the well-known emulator admin (local-emulator@stack-auth.com) using the PCK from ~/.stack/emulator/run/vm/internal-pck. Pass --cloud (or set STACK_EXEC_DEFAULT_TARGET=cloud) to hit the cloud API instead.
  • New STACK_EMULATOR_{BACKEND,DASHBOARD,MINIO,INBUCKET,MOCK_OAUTH}_PORT env vars take precedence over the legacy unprefixed EMULATOR_*_PORT names; the unprefixed names are still forwarded to run-emulator.sh.
  • Refactor: extract emulator paths/ports/PCK polling into packages/stack-cli/src/lib/emulator-paths.ts; hoist the shared local-emulator admin credentials into packages/stack-shared/src/local-emulator.ts so backend, dashboard auto-login, and CLI all agree on a single source.
  • Drop unused flags parameter from resolveLoginConfig / resolveSessionAuth / performLogin. getInternalApp reads publishableClientKey from the auth object so the local-emulator path can substitute the dynamically-read PCK.

Heads up — breaking change for existing scripts. Any CI or shell that currently runs stack exec '<js>' with STACK_PROJECT_ID + a refresh token will now try the local emulator and fail unless it passes --cloud or sets STACK_EXEC_DEFAULT_TARGET=cloud.

Test plan

  • pnpm lint passes
  • pnpm typecheck passes
  • Unit tests for resolveExecTarget, isRetryableFetchError, localEmulatorReadyTimeoutMs, pollInternalPck, and the STACK_-prefixed port resolution
  • e2e apps/e2e/tests/general/cli.test.ts:
    • local-default exec errors when emulator PCK file is missing (negative)
    • local-default exec errors when emulator API is unreachable (negative)
    • local-default exec runs against the local emulator backend (positive, gated on NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true)
    • Existing cloud-target tests pass after being updated to pass --cloud
  • Manual: stack emulator start then stack exec "return 1+1" returns 2 against the local emulator
  • Manual: stack exec --cloud "return 1+1" against a logged-in cloud session returns 2

Summary by CodeRabbit

  • New Features

    • stack exec adds a --cloud flag and STACK_EXEC_DEFAULT_TARGET to choose cloud vs local emulator
    • Dashboard/dev preview now auto-signs into the local emulator with shared dev credentials
    • New env keys to configure emulator API and dashboard URLs
  • Tests

    • Added CLI and emulator tests for exec targets, port resolution/fallbacks, and emulator sign-in
  • Refactor

    • Centralized emulator path/port handling and simplified CLI auth/session flows
  • Chores

    • Introduced shared dev-only emulator admin credentials for local workflows

Local-first dev workflow: `stack exec` now signs in as the emulator
admin via the well-known shared credentials and the run-dir PCK,
falling back to the cloud only when --cloud is passed (or
STACK_EXEC_DEFAULT_TARGET=cloud is set).

Also: STACK_EMULATOR_*_PORT env vars take precedence over the legacy
unprefixed names; emulator paths/ports/PCK polling extracted to
lib/emulator-paths.ts; shared local-emulator admin creds hoisted to
stack-shared so backend, dashboard auto-login, and CLI agree.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 8, 2026 6:28pm
stack-auth-mcp Ready Ready Preview, Comment May 8, 2026 6:28pm
stack-backend Ready Ready Preview, Comment May 8, 2026 6:28pm
stack-dashboard Ready Ready Preview, Comment May 8, 2026 6:28pm
stack-demo Ready Ready Preview, Comment May 8, 2026 6:28pm
stack-docs Ready Ready Preview, Comment May 8, 2026 6:28pm
stack-preview-backend Ready Ready Preview, Comment May 8, 2026 6:28pm
stack-preview-dashboard Ready Ready Preview, Comment May 8, 2026 6:28pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 662c071b-9dfa-4608-9113-d33e3c30e67a

📥 Commits

Reviewing files that changed from the base of the PR and between 497b9d2 and 00e17be.

📒 Files selected for processing (6)
  • apps/e2e/tests/general/cli.test.ts
  • packages/stack-cli/src/commands/emulator.ts
  • packages/stack-cli/src/commands/init.ts
  • packages/stack-cli/src/commands/project.ts
  • packages/stack-cli/src/lib/auth.ts
  • packages/stack-cli/src/lib/emulator-paths.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/stack-cli/src/commands/project.ts
  • packages/stack-cli/src/commands/init.ts
  • packages/stack-cli/src/lib/emulator-paths.ts
  • apps/e2e/tests/general/cli.test.ts
  • packages/stack-cli/src/commands/emulator.ts
  • packages/stack-cli/src/lib/auth.ts

📝 Walkthrough

Walkthrough

This PR centralizes local-emulator admin credentials in the shared package, adds emulator path/port utilities and PCK polling, implements a local-emulator auth flow with retries, refactors CLI auth helpers to be flag-less, adds cloud/local targeting for stack exec, and updates tests and integrations.

Changes

Local Emulator CLI Support

Layer / File(s) Summary
Shared Credentials
packages/stack-shared/src/local-emulator.ts, apps/backend/src/lib/local-emulator.ts, apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
Exports LOCAL_EMULATOR_ADMIN_EMAIL and LOCAL_EMULATOR_ADMIN_PASSWORD; backend re-exports and dashboard uses them for auto-login.
Emulator Path and Port Utilities
packages/stack-cli/src/lib/emulator-paths.ts
Adds default port constants, envPort, envPortFirstSet, emulator path helpers (emulatorHome, emulatorRunDir, internalPckPath), port accessors, and pollInternalPck() to wait for internal PCK readiness.
Emulator Paths Tests
packages/stack-cli/src/lib/emulator-paths.test.ts
Tests pollInternalPck behavior: trimmed read, timeout/null, whitespace handling, delayed-create detection, and non-ENOENT error propagation; isolates STACK_EMULATOR_HOME per test.
CLI Emulator Integration
packages/stack-cli/src/commands/emulator.ts
Replaces in-file port/path logic with imports from emulator-paths; readInternalPck() delegates to pollInternalPck(); emulatorSpawnEnv() forwards unprefixed EMULATOR_*_PORT vars; runtime ISO and welcome output use accessor helpers.
Emulator Port Tests
packages/stack-cli/src/commands/emulator.test.ts
Adds tests validating port-resolution precedence between STACK_* env vars and legacy EMULATOR_* aliases, default-port fallback, and invalid-value error handling.
Auth System Refactoring
packages/stack-cli/src/lib/auth.ts, packages/stack-cli/src/lib/auth.test.ts
LoginConfig adds publishableClientKey; resolveLoginConfig() and resolveSessionAuth() are parameterless; adds local-emulator URL resolution, localEmulatorReadyTimeoutMs(), isRetryableFetchError(), internal PCK polling, abortable sign-in with exponential backoff, and resolveLocalEmulatorAuth(). Tests cover retry classification and timeout parsing.
Exec Command Cloud/Local Targeting
packages/stack-cli/src/commands/exec.ts, packages/stack-cli/src/commands/exec.test.ts
Introduces ExecTarget and resolveExecTarget() to pick "cloud" or "local" via --cloud or STACK_EXEC_DEFAULT_TARGET; registerExecCommand() accepts --cloud and branches auth between resolveAuth() (cloud) and resolveLocalEmulatorAuth() (local). Tests verify defaulting, env-var parsing, flag precedence, and invalid-value handling.
Login/Init/Project Command Refactoring
packages/stack-cli/src/commands/login.ts, packages/stack-cli/src/commands/init.ts, packages/stack-cli/src/commands/project.ts
Removes flag threading: commands now call resolveLoginConfig() / resolveSessionAuth() without CLI flags; performLogin/ensureLoggedInSession refactored accordingly.
App and Backend Integration
packages/stack-cli/src/lib/app.ts, apps/backend/src/lib/local-emulator.ts, apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
Backend re-exports shared credentials; dashboard layout-client imports and uses them for emulator auto-login; CLI getInternalApp() uses session-provided publishableClientKey.
E2E and CLI Tests
apps/e2e/tests/general/cli.test.ts, various CLI test files
Adds isLocalEmulator gating, updates exec tests to pass --cloud for cloud flows, asserts --cloud in help, and adds local-default exec failure tests (missing PCK, unreachable API) plus a conditional positive local-emulator test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • nams1570

Poem

🐰 I hopped through ports and PCKs tonight,
shared creds tucked in the dev-only light,
exec can choose cloud or local with ease,
flags untangled, poll waits for keys to appease,
tests cheer — tiny rabbit does a dance of bytes.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main change: stack exec now defaults to the local emulator with --cloud as an opt-out.
Description check ✅ Passed The PR description comprehensively covers the changes, test plan, and breaking changes. It aligns with the provided template structure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cli-exec-changes

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/stack-cli/src/lib/emulator-paths.ts (1)

79-91: ⚡ Quick win

Replace Date.now() with performance.now() for elapsed-time measurement.

Date.now() is used here to compute a deadline and measure remaining time — exactly the use-case for performance.now().

As per coding guidelines: "Use performance.now() instead of Date.now() for measuring elapsed real time."

performance is a global in Node.js 16+; if the project must support earlier Node, import it from perf_hooks.

♻️ Proposed fix
 export async function pollInternalPck(timeoutMs: number): Promise<string | null> {
   const pckPath = internalPckPath();
-  const deadline = Date.now() + timeoutMs;
+  const deadline = performance.now() + timeoutMs;
   let delay = 50;
   while (true) {
     try {
       const contents = readFileSync(pckPath, "utf-8").trim();
       if (contents) return contents;
     } catch (e) {
       if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
     }
-    if (Date.now() >= deadline) return null;
-    const remaining = deadline - Date.now();
+    if (performance.now() >= deadline) return null;
+    const remaining = deadline - performance.now();
     await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
     delay = Math.min(delay * 2, 2000);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack-cli/src/lib/emulator-paths.ts` around lines 79 - 91, Replace
the uses of Date.now() used for elapsed-time/deadline calculation in the polling
loop inside emulator-paths.ts (the code that sets deadline = Date.now() +
timeoutMs and checks Date.now() >= deadline and computes remaining) with
performance.now() to measure elapsed time; ensure you either use the global
performance (Node 16+) or import { performance } from "perf_hooks" at the top if
globals are not available, and update all references (deadline, remaining
calculation, and delay scheduling) so they consistently use performance.now()
while leaving readFileSync(pckPath, "utf-8") and the retry/backoff logic
unchanged.
packages/stack-cli/src/commands/init.ts (1)

274-287: 💤 Low value

Drop the unused _flags parameter from these handlers.

_flags is no longer threaded into auth resolution, and both call sites in runInit already pass flags only because the signature still demands it. Removing the parameter (and the corresponding flags argument at the call sites) makes the obsolete data flow disappear instead of relying on the underscore convention.

♻️ Proposed fix
-async function handleCreateCloud(_flags: Record<string, unknown>, opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> {
+async function handleCreateCloud(opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> {
   const sessionAuth = await ensureLoggedInSession();
   ...
 }

-async function handleLinkFromCloud(_flags: Record<string, unknown>, opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> {
+async function handleLinkFromCloud(opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> {
   const sessionAuth = await ensureLoggedInSession();
   ...
 }

And update the call sites in runInit (lines 130/140) to drop flags. If flags is otherwise unused in runInit, you can also drop the const flags = program.opts(); line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack-cli/src/commands/init.ts` around lines 274 - 287, Remove the
unused _flags parameter from both handler signatures (handleCreateCloud and
handleLinkFromCloud) and update their call sites in runInit to stop passing
flags; specifically, delete the first parameter in each function definition and
remove the corresponding argument where they are invoked (and if runInit no
longer uses flags at all, also remove the const flags = program.opts()
declaration). Ensure only these two function names are changed so auth
resolution no longer receives obsolete flag data.
packages/stack-cli/src/lib/auth.ts (1)

217-220: 💤 Low value

Avoid silent catch-all on res.text().

.catch(() => "") swallows any failure (transport error mid-stream, abort, etc.) without observability. Since you're already inside an error-reporting branch, prefer explicit handling that surfaces the read failure in the resulting message instead of pretending the body was empty.

♻️ Proposed fix
   if (!res.ok) {
-    const body = await res.text().catch(() => "");
-    throw new AuthError(`Local emulator sign-in failed (${res.status} ${res.statusText})${body ? `: ${body}` : ""}. Make sure the emulator is running with NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true.`);
+    let body = "";
+    let bodyReadError: unknown = null;
+    try {
+      body = await res.text();
+    } catch (err) {
+      bodyReadError = err;
+    }
+    const detail = body
+      ? `: ${body}`
+      : bodyReadError
+        ? ` (failed to read response body: ${bodyReadError instanceof Error ? bodyReadError.message : String(bodyReadError)})`
+        : "";
+    throw new AuthError(`Local emulator sign-in failed (${res.status} ${res.statusText})${detail}. Make sure the emulator is running with NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true.`);
   }

As per coding guidelines, "NEVER try-catch-all, NEVER void a promise, and NEVER use .catch(console.error)." and "errors are never silently swallowed".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack-cli/src/lib/auth.ts` around lines 217 - 220, The current
catch-all on res.text() silences read failures; change the logic around the
failed response branch so you attempt to read the body but surface any body-read
error in the thrown AuthError instead of returning an empty string. Concretely,
wrap the await res.text() call in a try/catch that captures the thrown error
(e.g., bodyReadError) and include either the body or the
bodyReadError.message/stack in the new AuthError message (reference res,
res.text(), and AuthError) so transport/stream errors are visible in logs rather
than being silently swallowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/e2e/tests/general/cli.test.ts`:
- Around line 325-371: Add a guard assertion to both negative emulator tests
("local-default exec errors when emulator PCK file is missing" and
"local-default exec errors when emulator API is unreachable"): before calling
runCli, add expect(createdProjectId).toBeDefined() (the same check used in the
positive create-project test) so the tests fail fast if createdProjectId is
undefined and we don't mis-interpret a missing project ID as an emulator error;
update the tests that reference createdProjectId and runCli to include this
single-line guard.

In `@packages/stack-cli/src/lib/auth.ts`:
- Around line 179-202: Replace the wall-clock based timing in
localEmulatorSignInWithRetry with a monotonic clock: change all uses of
Date.now() that compute deadline, remainingForRequest, remaining, and the
deadline comparison to use performance.now() (keep units in milliseconds and the
same arithmetic with totalTimeoutMs), e.g. compute deadline = performance.now()
+ totalTimeoutMs and use performance.now() in the checks and mins for
perRequestTimeoutMs and sleep calculation; ensure lastError logic and thrown
message remain unchanged and no other behavior is altered.

---

Nitpick comments:
In `@packages/stack-cli/src/commands/init.ts`:
- Around line 274-287: Remove the unused _flags parameter from both handler
signatures (handleCreateCloud and handleLinkFromCloud) and update their call
sites in runInit to stop passing flags; specifically, delete the first parameter
in each function definition and remove the corresponding argument where they are
invoked (and if runInit no longer uses flags at all, also remove the const flags
= program.opts() declaration). Ensure only these two function names are changed
so auth resolution no longer receives obsolete flag data.

In `@packages/stack-cli/src/lib/auth.ts`:
- Around line 217-220: The current catch-all on res.text() silences read
failures; change the logic around the failed response branch so you attempt to
read the body but surface any body-read error in the thrown AuthError instead of
returning an empty string. Concretely, wrap the await res.text() call in a
try/catch that captures the thrown error (e.g., bodyReadError) and include
either the body or the bodyReadError.message/stack in the new AuthError message
(reference res, res.text(), and AuthError) so transport/stream errors are
visible in logs rather than being silently swallowed.

In `@packages/stack-cli/src/lib/emulator-paths.ts`:
- Around line 79-91: Replace the uses of Date.now() used for
elapsed-time/deadline calculation in the polling loop inside emulator-paths.ts
(the code that sets deadline = Date.now() + timeoutMs and checks Date.now() >=
deadline and computes remaining) with performance.now() to measure elapsed time;
ensure you either use the global performance (Node 16+) or import { performance
} from "perf_hooks" at the top if globals are not available, and update all
references (deadline, remaining calculation, and delay scheduling) so they
consistently use performance.now() while leaving readFileSync(pckPath, "utf-8")
and the retry/backoff logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7d488bb-4abd-4f8e-b7d7-f09370f56e56

📥 Commits

Reviewing files that changed from the base of the PR and between 6eaf492 and 6acd561.

📒 Files selected for processing (17)
  • apps/backend/src/lib/local-emulator.ts
  • apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
  • apps/e2e/tests/general/cli.test.ts
  • packages/stack-cli/src/commands/emulator.test.ts
  • packages/stack-cli/src/commands/emulator.ts
  • packages/stack-cli/src/commands/exec.test.ts
  • packages/stack-cli/src/commands/exec.ts
  • packages/stack-cli/src/commands/init.ts
  • packages/stack-cli/src/commands/login.ts
  • packages/stack-cli/src/commands/project.ts
  • packages/stack-cli/src/lib/app.ts
  • packages/stack-cli/src/lib/auth.test.ts
  • packages/stack-cli/src/lib/auth.ts
  • packages/stack-cli/src/lib/config.ts
  • packages/stack-cli/src/lib/emulator-paths.test.ts
  • packages/stack-cli/src/lib/emulator-paths.ts
  • packages/stack-shared/src/local-emulator.ts

Comment thread apps/e2e/tests/general/cli.test.ts
Comment thread packages/stack-cli/src/lib/auth.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR makes stack exec default to the local emulator (signing in as the well-known admin via a polled PCK file) and adds --cloud / STACK_EXEC_DEFAULT_TARGET=cloud to opt back to the cloud API. It also centralises emulator credentials into packages/stack-shared/src/local-emulator.ts, extracts path/port/PCK helpers into packages/stack-cli/src/lib/emulator-paths.ts, and drops the now-unused flags parameter from resolveLoginConfig / resolveSessionAuth / performLogin.

  • New emulator auth flow (auth.ts): polls the PCK file with exponential backoff, then signs in against the local backend with a retry loop; AuthError from either phase propagates cleanly to the top-level handler.
  • Port resolution (emulator-paths.ts): STACK_EMULATOR_*_PORT env vars now take precedence over the legacy unprefixed EMULATOR_*_PORT names; resolved values are forwarded to run-emulator.sh.
  • Breaking change (noted in the PR description): existing scripts that run stack exec without --cloud will target the local emulator and fail unless they pass --cloud or set STACK_EXEC_DEFAULT_TARGET=cloud.

Confidence Score: 4/5

Safe to merge after verifying the backend grants the emulator admin access to all local-emulator projects.

The auth-flow refactor is well-structured and the unit/e2e test coverage is thorough. The two concerns worth a second look: (1) the positive e2e test signs in as the emulator admin but looks up a project created by the test user — if the backend's listOwnedProjects does not give the admin superuser project access, that test will always fail in local-emulator CI; (2) the per-phase timeout budget is applied independently to PCK polling and sign-in, so the actual wait can silently reach double the configured value, and the 0ms fast-fail path still fires one 1ms-timeout network attempt that can produce an 'aborted' message rather than a connection-specific one. Neither blocks the cloud path or any existing functionality.

packages/stack-cli/src/lib/auth.ts (timeout semantics and error messaging in localEmulatorSignInWithRetry) and apps/e2e/tests/general/cli.test.ts (emulator-admin project-ownership assumption in the positive happy-path test).

Important Files Changed

Filename Overview
packages/stack-cli/src/commands/exec.ts Adds --cloud flag and resolveExecTarget to route between local-emulator and cloud paths; logic is straightforward and well-tested.
packages/stack-cli/src/lib/auth.ts Adds resolveLocalEmulatorAuth with PCK polling + sign-in retry loop; per-phase timeout budget can silently double worst-case wait; 0ms fast-fail path still makes one 1ms-timeout network attempt.
packages/stack-cli/src/lib/emulator-paths.ts New module cleanly extracts paths, port resolution (with STACK_-prefix alias priority), and PCK polling; well-covered by the accompanying test file.
packages/stack-shared/src/local-emulator.ts New shared module centralising the well-known emulator admin credentials; eliminates string duplication across backend, dashboard, and CLI.
apps/e2e/tests/general/cli.test.ts Existing cloud tests updated with --cloud; new negative emulator tests are sound; positive local-emulator test assumes emulator admin can see the test user's project — this works only if the backend grants the admin superuser project access.
packages/stack-cli/src/lib/app.ts Switches getInternalApp to use auth.publishableClientKey from the auth object instead of the hardcoded default; enables the local-emulator path to supply its own PCK.
apps/backend/src/lib/local-emulator.ts Removes the inline credential constants and imports them from the shared package; trivial refactor with no behavioural change.
apps/dashboard/src/app/(main)/(protected)/layout-client.tsx Replaces hardcoded credential strings with the shared constants; no behavioural change.
packages/stack-cli/src/commands/emulator.ts Removes inline port/path helpers in favour of imports from emulator-paths.ts; emulatorSpawnEnv now forwards all five resolved ports to run-emulator.sh regardless of source env var.
packages/stack-cli/src/lib/config.ts Adds STACK_EMULATOR_API_URL and STACK_EMULATOR_DASHBOARD_URL to the allowed ConfigKey union so they can be persisted in the credentials file.

Sequence Diagram

sequenceDiagram
    participant User
    participant exec.ts
    participant auth.ts
    participant emulator-paths.ts
    participant LocalEmulator

    User->>exec.ts: stack exec [--cloud] "js"
    exec.ts->>exec.ts: resolveExecTarget(opts, env)

    alt "--cloud or STACK_EXEC_DEFAULT_TARGET=cloud"
        exec.ts->>auth.ts: resolveAuth(flags)
        auth.ts-->>exec.ts: ProjectAuthWithRefreshToken (cloud creds)
    else default: local emulator
        exec.ts->>auth.ts: resolveLocalEmulatorAuth(flags)
        auth.ts->>emulator-paths.ts: pollInternalPck(readyTimeoutMs)
        emulator-paths.ts-->>auth.ts: internalPck (or null - throw AuthError)
        auth.ts->>LocalEmulator: POST /api/v1/auth/password/sign-in (retry loop)
        LocalEmulator-->>auth.ts: refresh_token
        auth.ts-->>exec.ts: ProjectAuthWithRefreshToken (local creds)
    end

    exec.ts->>exec.ts: getAdminProject(auth)
    exec.ts->>LocalEmulator: listOwnedProjects - find projectId
    exec.ts->>exec.ts: new AsyncFunction(stackServerApp, js)
    exec.ts-->>User: JSON result or error
Loading

Comments Outside Diff (3)

  1. apps/e2e/tests/general/cli.test.ts, line 249-269 (link)

    P2 Emulator admin ownership assumption may not hold

    The positive happy-path test signs in as local-emulator@stack-auth.com and then calls getAdminProject(auth), which internally calls user.listOwnedProjects() and looks for createdProjectId. However, createdProjectId was created by the test user (a randomly generated account from beforeAll), not by the emulator admin. If the backend's listOwnedProjects for the emulator admin only returns projects that user actually owns, getAdminProject will throw "Project '...' not found. Make sure you own this project." and the test will fail.

    The test only runs when isLocalEmulator is true, suggesting the backend may grant the emulator admin superuser-level project access. If so, it would be worth a short comment here (or in getAdminProject) explaining that assumption, so future readers don't have to dig through backend code to understand why this works.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/e2e/tests/general/cli.test.ts
    Line: 249-269
    
    Comment:
    **Emulator admin ownership assumption may not hold**
    
    The positive happy-path test signs in as `local-emulator@stack-auth.com` and then calls `getAdminProject(auth)`, which internally calls `user.listOwnedProjects()` and looks for `createdProjectId`. However, `createdProjectId` was created by the test user (a randomly generated account from `beforeAll`), not by the emulator admin. If the backend's `listOwnedProjects` for the emulator admin only returns projects that user actually owns, `getAdminProject` will throw "Project '...' not found. Make sure you own this project." and the test will fail.
    
    The test only runs when `isLocalEmulator` is true, suggesting the backend may grant the emulator admin superuser-level project access. If so, it would be worth a short comment here (or in `getAdminProject`) explaining that assumption, so future readers don't have to dig through backend code to understand why this works.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. packages/stack-cli/src/lib/auth.ts, line 904-927 (link)

    P2 Network attempt still made when totalTimeoutMs = 0

    When STACK_EMULATOR_READY_TIMEOUT_MS=0 (used in the "unreachable" e2e test to fail fast), deadline = Date.now() and remainingForRequest = Math.max(1, 0-ish) = 1. This forces one fetch attempt with a 1ms AbortSignal.timeout. If the 1ms signal fires before ECONNREFUSED is returned (e.g., on a slow CI machine), lastError becomes an AbortError with message = "aborted", and the thrown AuthError reads "Cannot reach local emulator … (after 0ms): aborted." — which hides the actual connection error.

    For "fail-fast" callers, adding an early-exit when totalTimeoutMs === 0 before the loop would skip the misleading 1ms-timeout fetch entirely and produce a cleaner message.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/stack-cli/src/lib/auth.ts
    Line: 904-927
    
    Comment:
    **Network attempt still made when `totalTimeoutMs = 0`**
    
    When `STACK_EMULATOR_READY_TIMEOUT_MS=0` (used in the "unreachable" e2e test to fail fast), `deadline = Date.now()` and `remainingForRequest = Math.max(1, 0-ish) = 1`. This forces one fetch attempt with a `1ms` `AbortSignal.timeout`. If the 1ms signal fires before ECONNREFUSED is returned (e.g., on a slow CI machine), `lastError` becomes an `AbortError` with `message = "aborted"`, and the thrown `AuthError` reads `"Cannot reach local emulator … (after 0ms): aborted."` — which hides the actual connection error.
    
    For "fail-fast" callers, adding an early-exit when `totalTimeoutMs === 0` before the loop would skip the misleading 1ms-timeout fetch entirely and produce a cleaner message.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. packages/stack-cli/src/lib/auth.ts, line 848-866 (link)

    P2 Double-timeout budget not surfaced in user-visible error messages

    The comment correctly notes that readyTimeoutMs is applied independently to PCK polling and the sign-in retry loop, so wall-clock time can reach ~2× the configured value. However, localEmulatorSignInWithRetry reports "(after ${totalTimeoutMs}ms)" using the per-phase budget, not the elapsed real time. A user who sets STACK_EMULATOR_READY_TIMEOUT_MS=5000 and hits both phase timeouts will wait 10 seconds but see "(after 5000ms)" in the error — which understates the actual wait. Tracking and reporting real elapsed time (or documenting the 2× behaviour in the error string) would reduce confusion.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/stack-cli/src/lib/auth.ts
    Line: 848-866
    
    Comment:
    **Double-timeout budget not surfaced in user-visible error messages**
    
    The comment correctly notes that `readyTimeoutMs` is applied independently to PCK polling and the sign-in retry loop, so wall-clock time can reach ~2× the configured value. However, `localEmulatorSignInWithRetry` reports `"(after ${totalTimeoutMs}ms)"` using the per-phase budget, not the elapsed real time. A user who sets `STACK_EMULATOR_READY_TIMEOUT_MS=5000` and hits both phase timeouts will wait 10 seconds but see `"(after 5000ms)"` in the error — which understates the actual wait. Tracking and reporting real elapsed time (or documenting the 2× behaviour in the error string) would reduce confusion.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/e2e/tests/general/cli.test.ts:249-269
**Emulator admin ownership assumption may not hold**

The positive happy-path test signs in as `local-emulator@stack-auth.com` and then calls `getAdminProject(auth)`, which internally calls `user.listOwnedProjects()` and looks for `createdProjectId`. However, `createdProjectId` was created by the test user (a randomly generated account from `beforeAll`), not by the emulator admin. If the backend's `listOwnedProjects` for the emulator admin only returns projects that user actually owns, `getAdminProject` will throw "Project '...' not found. Make sure you own this project." and the test will fail.

The test only runs when `isLocalEmulator` is true, suggesting the backend may grant the emulator admin superuser-level project access. If so, it would be worth a short comment here (or in `getAdminProject`) explaining that assumption, so future readers don't have to dig through backend code to understand why this works.

### Issue 2 of 3
packages/stack-cli/src/lib/auth.ts:904-927
**Network attempt still made when `totalTimeoutMs = 0`**

When `STACK_EMULATOR_READY_TIMEOUT_MS=0` (used in the "unreachable" e2e test to fail fast), `deadline = Date.now()` and `remainingForRequest = Math.max(1, 0-ish) = 1`. This forces one fetch attempt with a `1ms` `AbortSignal.timeout`. If the 1ms signal fires before ECONNREFUSED is returned (e.g., on a slow CI machine), `lastError` becomes an `AbortError` with `message = "aborted"`, and the thrown `AuthError` reads `"Cannot reach local emulator … (after 0ms): aborted."` — which hides the actual connection error.

For "fail-fast" callers, adding an early-exit when `totalTimeoutMs === 0` before the loop would skip the misleading 1ms-timeout fetch entirely and produce a cleaner message.

### Issue 3 of 3
packages/stack-cli/src/lib/auth.ts:848-866
**Double-timeout budget not surfaced in user-visible error messages**

The comment correctly notes that `readyTimeoutMs` is applied independently to PCK polling and the sign-in retry loop, so wall-clock time can reach ~2× the configured value. However, `localEmulatorSignInWithRetry` reports `"(after ${totalTimeoutMs}ms)"` using the per-phase budget, not the elapsed real time. A user who sets `STACK_EMULATOR_READY_TIMEOUT_MS=5000` and hits both phase timeouts will wait 10 seconds but see `"(after 5000ms)"` in the error — which understates the actual wait. Tracking and reporting real elapsed time (or documenting the 2× behaviour in the error string) would reduce confusion.

Reviews (1): Last reviewed commit: "stack exec: default to local emulator, a..." | Re-trigger Greptile

Comment thread packages/stack-cli/src/lib/auth.ts
- The positive happy-path test minted a project owned by the test user's
  team, but the CLI signs in as the local-emulator admin whose
  listOwnedProjects() only returns LOCAL_EMULATOR_OWNER_TEAM_ID-owned
  projects. Mint the project via /internal/local-emulator/project so it
  shows up under the admin's team.
- Surface stderr when the positive test exits non-zero so future
  regressions report the real CLI error instead of a bare exit-code mismatch.
- Add expect(createdProjectId).toBeDefined() guards to the two negative
  emulator tests for parity with the positive test.
- Use performance.now() instead of Date.now() for the local-emulator
  sign-in retry deadline so wall-clock skew can't break the loop.
Comment thread packages/stack-cli/src/lib/auth.ts Outdated
Comment thread packages/stack-cli/src/lib/emulator-paths.ts Outdated
BilalG1 added 2 commits May 8, 2026 11:00
# Conflicts:
#	packages/stack-cli/src/commands/init.ts
- Use performance.now() in pollInternalPck for the polling deadline so
  wall-clock skew can't break the loop, mirroring the same change in
  localEmulatorSignInWithRetry.
- Surface response-body read failures in resolveLocalEmulatorAuth instead
  of swallowing them with .catch(() => ""). The original message
  ("Local emulator sign-in failed (status text)") loses all diagnostic
  info when res.text() itself throws; now we throw an AuthError that
  includes the read error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants